<?php
/**
 * PrynLib
 * @copyright 2007-2009 Prynel
 * @author Olivier ROGER <roger.olivier@gmail.com>
 * @category libPrynel
 * @package Db
 * @version $Revision: 80 $
 */

/**
 * Classe Représentation intervallaire
 *
 * Permet la gestion de table à représentation intervallaire
 *
 * @category libPrynel
 * @package Db
 * @version 0.9.5 
 * @author Olivier ROGER <roger.olivier@gmail.com>
 * @copyright  2007-2009 Prynel
 *        
 * <code>
 * $interval = new Db_NestedTree('category');
 * echo $interval->getHTMLTree();
 * $interval->setCurrent(5);
 * $interval->addChild('Fils de 5');
 * $internal->getChilds(); // Retourne tous les enfant de 5
 * $interval->getChilds(Db_NestedTree::LEAFONLY); // REtourne tous les enfant de 5 qui ne sont pas des noeuds
 * </code>
 * @todo suppression de noeud
 */
class Db_NestedTree
{
	/**
	 * Objet SPDO
	 *
	 * @var Db_Spdo
	 */
	private $oSql;
	private $label;
	private $leftBound;
	private $rightBound;
	private $tableName;
	private $id;
	private $current;
	
	const LEAFONLY = 1;
	const NODEONLY = 2;
	
	/**
	 * Initialise la représentation
	 *
	 * @param string $table nom de la table
	 * @param string $label Nom du champs label
	 * @param string $left Nom du champs de borne gauche
	 * @param string $right Nom du champs de borne droite
	 * @param string $id Nom du champs id
	 * @access public
	 */
	public function __construct($table,$label='nom',$left='left',$right='right',$id='id')
	{
		$this->oSql 		= Db_Spdo::getInstance();
		$this->tableName	= $table;
		$this->leftBound 	= $left;
		$this->rightBound 	= $right;
		$this->id			= $id;
		$this->label		= $label;
		$this->current		= array();
	}
	
	/**
	 * Défini l'élément de travail
	 *
	 * @param int $id Id de l'élément
	 * @access public
	 */
	public function setCurrent($id)
	{
		$prepare = $this->oSql->prepare('SELECT `'.$this->leftBound.'`,`'.$this->rightBound.'` 
									  FROM '.$this->tableName.' 
									  WHERE `'.$this->id.'` =:id');
		$prepare->execute(array(':id'=>$id));
		$current = $prepare->fetch(PDO::FETCH_ASSOC);		   
		$this->current['left'] 	= intval($current['left']);
		$this->current['right'] = intval($current['right']);
	}
	
	/**
	 * Ajoute un élément racine.
	 * Permet de débuter un arbre
	 *
	 * @param string $nom Nom de l'élément
	 * @access public
	 */
	public function addRootElement($nom)
	{
		//Existe il déjà un élément root ?
		$right = $this->oSql->query('SELECT `'.$this->rightBound.'` FROM '.$this->tableName.' WHERE `'.$this->leftBound.'` = 1')->fetchColumn(0);
		if(!is_null($right))
		{
			$leftB 	= $right+1;
			$rightB	= $right+2; 
		}
		else
		{
			$leftB 	= 1;
			$rightB = 2;
		}
		$prepare = $this->oSql->prepare('INSERT INTO '.$this->tableName.' (`'.$this->label.'`,`'.$this->leftBound.'`,`'.$this->rightBound.'`) 
							VALUES(:nom,'.$leftB.','.$rightB.')');
		$prepare->execute(array(':nom'=>$nom));
	}
	
	/**
	 * Retourne un tableau de l'arborescence avec des niveaux de profondeur
	 * 
	 * @access public
	 * @return array
	 */
	public function getTree()
	{
		$data= $this->oSql->query('SELECT node.'.$this->label.', (COUNT(parent.'.$this->label.')-1) as depth 
							FROM '.$this->tableName.' as node,'.$this->tableName.' as parent 
							WHERE node.'.$this->leftBound.' BETWEEN parent.'.$this->leftBound.' AND parent.'.$this->rightBound.' 
							GROUP BY node.'.$this->label.'
							ORDER BY node.'.$this->leftBound.',depth');
		while($result = $data->fetch(PDO::FETCH_ASSOC))
			$tree[] = $result;
			
		return $tree;
	}
	
	/**
	 * Retourne l'arborescence sous forme de balise HTML
	 *
	 * @access public
	 * @return string
	 */
	public function getHTMLTree()
	{
		$tree 		= $this->getTree();
		//var_dump($tree);
		$depth 		= 0;
		$htmlTree 	= '';
		$htmlTree	.='<ul>';
		foreach($tree as $value)
		{
			if($depth<$value['depth'])
			{
				$htmlTree	.=str_repeat('<ul>',($value['depth']-$depth));
				$depth 		= $value['depth'];
				$htmlTree 	.= '<li>'.$value['nom'].'</li>';
			}
			elseif($depth>$value['depth'])
			{
				$htmlTree	.= str_repeat('</ul>',++$depth - $value['depth']);
				$depth 		= $value['depth'];
				$htmlTree	.='<ul><li>'.$value['nom'].'</li>';
			}
			else
				$htmlTree	.='<li>'.$value['nom'].'</li>';
		}
		$htmlTree.='</ul>';
		return $htmlTree;
	}
	
	/**
	 * Compte les enfants de l'élément
	 * 
	 * @param int $option Permet de choisir si on veut uniquement les noeuds ou les feuilles
	 * @access public
	 * @return int
	 */
	public function countChilds($option=null)
	{	   			 
		$this->checkCurrent();
		
		if(!empty($option))
		{
			if(self::LEAFONLY == $option)
				$option = ' AND (`'.$this->rightBound.'`-`'.$this->leftBound.'`)=1';
			elseif(self::NODEONLY == $option)
				$option = ' AND (`'.$this->rightBound.'`-`'.$this->leftBound.'`)>1';
			else
				$option = '';
		}
		else
			$option = '';
			
		$child = $this->oSql->query('SELECT COUNT(*) 
									  FROM '.$this->tableName.'
									  WHERE `'.$this->leftBound.'`>'.$this->current['left'].' 
									  	AND `'.$this->rightBound.'` < '.$this->current['right'].$option)
							 ->fetchColumn(0);
		return $child;
	}
	
	/**
	 * Retourne l'id et le label des enfants de l'éléments.
	 * 
	 * @param int $option Permet de choisir si on veut uniquement les noeud ou les feuilles
	 * @access public
	 * @return array
	 */
	public function getChilds($option=null)
	{
		$this->checkCurrent();
		
		$childs = array();
		if(!empty($option))
		{
			if(self::LEAFONLY == $option)
				$option = ' AND (`'.$this->rightBound.'`-`'.$this->leftBound.'`)=1';
			elseif(self::NODEONLY == $option)
				$option = ' AND (`'.$this->rightBound.'`-`'.$this->leftBound.'`)>1';
			else
				$option = '';
		}
		else
			$option = '';
			
		$child 	= $this->oSql->query('SELECT '.$this->id.','.$this->label.' 
									  FROM '.$this->tableName.'
									  WHERE `'.$this->leftBound.'`>'.$this->current['left'].' 
									  	AND `'.$this->rightBound.'` < '.$this->current['right'].$option);
		while($result = $child->fetch(PDO::FETCH_ASSOC))
			$childs[] = $result;
			
		return $childs;
	}
	
	/**
	 * Ajoute un enfant (par la droite) à l'élément
	 *
	 * @param string $nom Nom de l'élément à ajouter
	 * @access public
	 * @return boolean
	 */
	public function addChild($nom)
	{
		$this->checkCurrent();
		
		// insertion après la borne droite du père
		$this->oSql->query('UPDATE '.$this->tableName.' 
							SET `'.$this->rightBound.'`=`'.$this->rightBound.'`+2
							WHERE `'.$this->rightBound.'`>='.$this->current['right']);
		// 2 requetes de décalage pour éviter les problème d'unicité
		$this->oSql->query('UPDATE '.$this->tableName.' 
							SET `'.$this->leftBound.'`=`'.$this->leftBound.'`+2
							WHERE `'.$this->leftBound.'`>='.$this->current['right']);
		
		
		$prepare = $this->oSql->prepare('INSERT INTO '.$this->tableName.' (`'.$this->label.'`,`'.$this->leftBound.'`,`'.$this->rightBound.'`)
									VALUES(:nom,'.$this->current['right'].','.($this->current['right']+1).')');
		$prepare->execute(array(':nom'=>$nom));
		
		$this->current['right']+=2;
		return $data;
	}
	
	/**
	 * Supprime l'élément courant si c'est une feuille
	 *
	 * @access public
	 * @return boolean
	 */
	public function delete()
	{
		$this->checkCurrent();
		
		if(($this->current['right']-$this->current['left']==1))
		{
			$this->oSql->query('DELETE FROM '.$this->tableName.' 
								WHERE `'.$this->leftBound.'` = '.$this->current['left']);
			
			$this->oSql->query('UPDATE '.$this->tableName.' 
								SET `'.$this->leftBound.'`=`'.$this->leftBound.'`-2
								WHERE `'.$this->leftBound.'`>='.$this->current['left']);
			
			$this->oSql->query('UPDATE '.$this->tableName.' 
								SET `'.$this->rightBound.'`=`'.$this->rightBound.'`-2
								WHERE `'.$this->rightBound.'`>='.$this->current['left']);
			return true;
		}
		else
		{
			return false;
		}
	}
	
	/**
	 * Compte les parents de l'élément
	 * @access public
	 * @return int
	 */
	public function countParents()
	{
		$this->checkCurrent();
		
		$parent = $this->oSql->query('SELECT COUNT(*) 
									  FROM '.$this->tableName.'
									  WHERE `'.$this->leftBound.'`<'.$this->current['left'].' 
									  	AND `'.$this->rightBound.'` > '.$this->current['right'])
							 ->fetchColumn(0);
		return $parent;
	}
	
	/**
	 * Retourne les parents de l'élément
	 *
	 * @access public
	 * @return array
	 */
	public function getParents()
	{
		$this->checkCurrent();
		
		$parents = array();
		$parent = $this->oSql->query('SELECT '.$this->id.','.$this->label.' 
									  FROM '.$this->tableName.'
									  WHERE `'.$this->leftBound.'`<'.$this->current['left'].' 
									  	AND `'.$this->rightBound.'` > '.$this->current['right']);
		
		while($result = $parent->fetch(PDO::FETCH_ASSOC))
			$parents[] = $result;
			
		return $parents;
	}
	
	/**
	 * Vérifie si un élément est séléctionné
	 * @access private
	 */
	private function checkCurrent()
	{
		if(empty($this->current))
		{
			throw new Util_ExceptionHandler('Aucun élément courant n\'est défini');
		}
	}
}
 ?>